﻿using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;

namespace NthDownload.Common.Collections
{
    /// <summary>
    ///  Class used to implement a collection view source which is able to automatically request a smart refresh an the associated view when an item changes.
    /// </summary>
    public class AutoRefreshCollectionViewSource : CollectionViewSource
    {
        /// <summary>
        ///  Called when the source has changed.
        /// </summary>
        /// <param name = "oldSource">The old source.</param>
        /// <param name = "newSource">The new source.</param>
        protected override void OnSourceChanged(object oldSource, object newSource)
        {
            if (oldSource != null)
            {
                UnsubscribeSourceEvents(oldSource);
            }

            if (newSource != null)
            {
                SubscribeSourceEvents(newSource);
            }

            base.OnSourceChanged(oldSource, newSource);
        }

        /// <summary>
        ///  Unsubscribes from the source events.
        /// </summary>
        /// <param name = "source">The source.</param>
        private void UnsubscribeSourceEvents(object source)
        {
            var notify = source as INotifyCollectionChanged;

            if (notify != null)
            {
                notify.CollectionChanged -= OnSourceCollectionChanged;
            }

            if (source is IEnumerable)
            {
                UnsubscribeItemsEvents((IEnumerable)source);
            }
        }

        /// <summary>
        ///  Subscribes to the source events.
        /// </summary>
        /// <param name = "source">The source.</param>
        private void SubscribeSourceEvents(object source)
        {
            var notify = source as INotifyCollectionChanged;

            if (notify != null)
            {
                notify.CollectionChanged += OnSourceCollectionChanged;
            }

            if (source is IEnumerable)
            {
                SubscribeItemsEvents((IEnumerable)source);
            }
        }

        /// <summary>
        ///  Unsubscribes from the item events.
        /// </summary>
        /// <param name = "item">The item.</param>
        private void UnsubscribeItemEvents(object item)
        {
            var notify = item as INotifyPropertyChanged;

            if (notify != null)
            {
                notify.PropertyChanged -= OnItemPropertyChanged;
            }
        }

        /// <summary>
        ///  Subscribes to the item events.
        /// </summary>
        /// <param name = "item">The item.</param>
        private void SubscribeItemEvents(object item)
        {
            var notify = item as INotifyPropertyChanged;

            if (notify != null)
            {
                notify.PropertyChanged += OnItemPropertyChanged;
            }
        }

        /// <summary>
        ///  Unsubscribes from the items events.
        /// </summary>
        /// <param name = "items">The items.</param>
        private void UnsubscribeItemsEvents(IEnumerable items)
        {
            foreach (object item in items)
            {
                UnsubscribeItemEvents(item);
            }
        }

        /// <summary>
        ///  Subscribes to the items events.
        /// </summary>
        /// <param name = "items">The items.</param>
        private void SubscribeItemsEvents(IEnumerable items)
        {
            foreach (object item in items)
            {
                SubscribeItemEvents(item);
            }
        }

        /// <summary>
        ///  Called when a source collection has changed.
        /// </summary>
        /// <param name = "sender">The sender.</param>
        /// <param name = "e">The <see cref = "System.Collections.Specialized.NotifyCollectionChangedEventArgs" /> instance containing the event data.</param>
        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //If the collection view is cleared, events should be detached.
            //Failing to do so would case the collection view itself to linger in memory until all the removed items are garbage collected.
            //If weak events are used, there is not such risk, so it safe to ignore the problem.
            //Note anyway that if both the collection view and the removed objects are used after a Reset operation, it is possible
            //for the collection view to process unneeded events. This should just a bit of unneeded overhead.
            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                throw new InvalidOperationException(string.Format("The action {0} is not supported by {1}", e.Action, GetType()));
            }

            if (e.NewItems != null)
            {
                SubscribeItemsEvents(e.NewItems);
            }

            if (e.OldItems != null)
            {
                UnsubscribeItemsEvents(e.OldItems);
            }
        }

        /// <summary>
        ///  Called when an item property has changed.
        /// </summary>
        /// <param name = "sender">The sender.</param>
        /// <param name = "e">The <see cref = "System.ComponentModel.PropertyChangedEventArgs" /> instance containing the event data.</param>
        private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (IsViewRefreshNeeded(e.PropertyName))
            {
                var view = View;
                if (view != null)
                {
                    var current = view.CurrentItem;
                    var editableCollectionView = view as IEditableCollectionView;

                    if (editableCollectionView != null)
                    {
                        editableCollectionView.EditItem(sender);
                        editableCollectionView.CommitEdit();
                    }
                    else
                    {
                        view.Refresh();
                    }

                    view.MoveCurrentTo(current);
                    //Ensure that the previously current item is maintained after the refresh operation
                }
            }
        }

        /// <summary>
        ///  Determines whether a view refresh is needed.
        /// </summary>
        /// <param name = "propertyName">The name of the changed property.</param>
        /// <returns>
        ///  <c>True</c> if a view refresh needed; otherwise, <c>false</c>.
        /// </returns>
        private bool IsViewRefreshNeeded(string propertyName)
        {
            return SortDescriptions.Any(sort => string.Equals(sort.PropertyName, propertyName)) || GroupDescriptions.OfType<PropertyGroupDescription>().Where(g => string.Equals(g.PropertyName, propertyName)).Any();
        }
    }
}
